home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / hity wydania / Ubuntu 9.10 PL / karmelkowy-koliberek-desktop-9.10-i386-PL.iso / casper / filesystem.squashfs / usr / share / pyshared / jockey / oslib.py < prev    next >
Text File  |  2009-10-25  |  22KB  |  598 lines

  1. # -*- coding: UTF-8 -*-
  2. # (c) 2007 Canonical Ltd.
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License along
  15. # with this program; if not, write to the Free Software Foundation, Inc.,
  16. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  17.  
  18. '''Encapsulate operations which are Linux distribution specific.'''
  19.  
  20. import fcntl, os, subprocess, sys, logging, re, tempfile
  21. from glob import glob
  22.  
  23. import warnings
  24. warnings.simplefilter('ignore', FutureWarning)
  25. import apt
  26.  
  27. class _CapturedInstallProgress(apt.InstallProgress):
  28.     def fork(self):
  29.         '''Reroute stdout/stderr to files, so that we can log them'''
  30.  
  31.         self.stdout = tempfile.TemporaryFile()
  32.         self.stderr = tempfile.TemporaryFile()
  33.         p = os.fork()
  34.         if p == 0:
  35.             os.dup2(self.stdout.fileno(), sys.stdout.fileno())
  36.             os.dup2(self.stderr.fileno(), sys.stderr.fileno())
  37.         return p
  38.  
  39. class OSLib:
  40.     '''Encapsulation of operating system/Linux distribution specific operations.'''
  41.  
  42.     # global default instance
  43.     inst = None
  44.  
  45.     def __init__(self, client_only=False):
  46.         '''Set default paths and load the module blacklist.
  47.         
  48.         Distributors might want to override some default paths.
  49.         If client_only is True, this only initializes functionality which is
  50.         needed by clients, and which can be done without special privileges.
  51.         '''
  52.         # relevant stuff for clients and backend
  53.         self._get_os_version()
  54.         self.hal_get_property_path = '/usr/bin/hal-get-property'
  55.  
  56.         if client_only:
  57.             return
  58.  
  59.         # below follows stuff which is only necessary for the backend
  60.  
  61.         # default paths
  62.  
  63.         # /sys/ path; the main purpose of changing this is for test
  64.         # suites, but some vendors might have /sys in a nonstandard place
  65.         self.sys_dir = '/sys'
  66.  
  67.         # path to a modprobe.d configuration file where kernel modules are
  68.         # enabled and disabled with blacklisting
  69.         self.module_blacklist_file = '/etc/modprobe.d/blacklist-local.conf'
  70.  
  71.         # path to modinfo binary
  72.         self.modinfo_path = '/sbin/modinfo'
  73.  
  74.         # path to modprobe binary
  75.         self.modprobe_path = '/sbin/modprobe'
  76.  
  77.         # path to kernel's list of loaded modules
  78.         self.proc_modules = '/proc/modules'
  79.  
  80.         # default path to custom handlers
  81.         self.handler_dir = '/usr/share/jockey/handlers'
  82.  
  83.         # default paths to modalias files (directory entries will consider all
  84.         # files in them)
  85.         self.modaliases = [
  86.             '/lib/modules/%s/modules.alias' % os.uname()[2],
  87.             '/usr/share/jockey/modaliases/',
  88.         ]
  89.  
  90.         # path to X.org configuration file
  91.         self.xorg_conf_path = '/etc/X11/xorg.conf'
  92.  
  93.         self.set_backup_dir()
  94.  
  95.         # cache file for previously seen/newly used handlers lists (for --check)
  96.         self.check_cache = os.path.join(self.backup_dir, 'check')
  97.  
  98.         self._load_module_blacklist()
  99.  
  100.         self.apt_show_cache = {}
  101.         self.apt_sources = '/etc/apt/sources.list'
  102.         self.apt_jockey_source = '/etc/apt/sources.list.d/jockey.list'
  103.  
  104.     # 
  105.     # The following package related functions use PackageKit; if that does not
  106.     # work for your distribution, they must be reimplemented
  107.     #
  108.  
  109.     def _apt_show(self, package):
  110.         '''Return apt-cache show output, with caching.
  111.         
  112.         Return None if the package does not exist.
  113.         '''
  114.         try:
  115.             return self.apt_show_cache[package]
  116.         except KeyError:
  117.             apt = subprocess.Popen(['apt-cache', 'show', package],
  118.                 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  119.             out = apt.communicate()[0].strip()
  120.             if apt.returncode == 0 and out:
  121.                 result = out
  122.             else:
  123.                 result = None
  124.             self.apt_show_cache[package] = result
  125.             return result
  126.  
  127.     def is_package_free(self, package):
  128.         '''Return if given package is free software.'''
  129.  
  130.         # TODO: this only works for packages in the official archive
  131.         out = self._apt_show(package)
  132.         if out:
  133.             for l in out.splitlines():
  134.                 if l.startswith('Section:'):
  135.                     s = l.split()[-1]
  136.                     return not (s.startswith('restricted') or s.startswith('multiverse'))
  137.  
  138.         raise ValueError, 'package %s does not exist' % package
  139.  
  140.     def package_installed(self, package):
  141.         '''Return if the given package is installed.'''
  142.  
  143.         dpkg = subprocess.Popen(["dpkg-query", "-W", "-f${Status}", package],
  144.             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  145.         out = dpkg.communicate()[0]
  146.         return dpkg.returncode == 0 and out.split()[-1] == "installed"
  147.  
  148.     def package_description(self, package):
  149.         '''Return a tuple (short_description, long_description) for a package.
  150.         
  151.         This should raise a ValueError if the package is not available.
  152.         '''
  153.         out = self._apt_show(package)
  154.         if out:
  155.             lines = out.splitlines()
  156.             start = 0
  157.             while start < len(lines)-1:
  158.                 if lines[start].startswith('Description:'):
  159.                     break
  160.                 start += 1
  161.  
  162.             short = lines[start].split(' ', 1)[1]
  163.             long = ''
  164.             for l in lines[start+1:]:
  165.                 if l == ' .':
  166.                     long += '\n\n'
  167.                 elif l.startswith(' '):
  168.                     long += l.lstrip()
  169.                 else:
  170.                     break
  171.  
  172.             return (short, long)
  173.  
  174.         raise ValueError, 'package %s does not exist' % package
  175.  
  176.     def package_files(self, package):
  177.         '''Return a list of files shipped by a package.
  178.         
  179.         This should raise a ValueError if the package is not installed.
  180.         '''
  181.         pkcon = subprocess.Popen(['dpkg', '-L', package],
  182.             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  183.         out = pkcon.communicate()[0]
  184.         if pkcon.returncode == 0:
  185.             return out.splitlines()
  186.         else:
  187.             raise ValueError, 'package %s is not installed' % package
  188.  
  189.     def install_package(self, package, progress_cb):
  190.         '''Install the given package.
  191.  
  192.         As this is called in the backend, this must happen noninteractively.
  193.         For progress reporting, progress_cb(phase, current, total) is called
  194.         regularly, with 'phase' being 'download' or 'install'. If the callback
  195.         returns True, the installation is attempted to get cancelled (this
  196.         will probably succeed in the 'download' phase, but not in 'install').
  197.         Passes '-1' for current and/or total if time cannot be determined.
  198.  
  199.         If this succeeds, subsequent package_installed(package) calls must
  200.         return True.
  201.  
  202.         Any installation failure should be raised as a SystemError.
  203.         '''
  204.         class MyFetchProgress(apt.FetchProgress):
  205.             def __init__(self, callback):
  206.                 apt.FetchProgress.__init__(self)
  207.                 self.callback = callback
  208.  
  209.             def pulse(self):
  210.                 return not self.callback('download', int(self.percent/2+.5), 100)
  211.  
  212.         class MyInstallProgress(_CapturedInstallProgress):
  213.             def __init__(self, callback):
  214.                 _CapturedInstallProgress.__init__(self)
  215.                 self.callback = callback
  216.  
  217.             def statusChange(self, pkg, percent, status):
  218.                 logging.debug('install progress statusChange %s %f' % (pkg, percent))
  219.                 self.callback('install', int(percent/2+50.5), 100)
  220.  
  221.         logging.debug('Installing package: %s', package)
  222.         if progress_cb:
  223.             progress_cb('download', 0, 100.0)
  224.  
  225.         os.environ['DEBIAN_FRONTEND'] = 'noninteractive'
  226.         os.environ['PATH'] = '/sbin:/usr/sbin:/bin:/usr/bin'
  227.         apt.apt_pkg.Config.Set('DPkg::options::','--force-confnew')
  228.  
  229.         c = apt.Cache()
  230.         try:
  231.             try:
  232.                 c[package].markInstall()
  233.             except KeyError:
  234.                 logging.debug('Package %s does not exist, aborting', package)
  235.                 return False
  236.             inst_p = progress_cb and MyInstallProgress(progress_cb) or None
  237.             c.commit(progress_cb and MyFetchProgress(progress_cb) or None, inst_p)
  238.             if inst_p:
  239.                 inst_p.stdout.seek(0)
  240.                 out = inst_p.stdout.read()
  241.                 inst_p.stdout.close()
  242.                 inst_p.stderr.seek(0)
  243.                 err = inst_p.stderr.read()
  244.                 inst_p.stderr.close()
  245.  
  246.                 if out:
  247.                     logging.debug(out)
  248.                 if err:
  249.                     logging.error(err)
  250.         except apt.cache.FetchCancelledException, e:
  251.             return False
  252.         except (apt.cache.LockFailedException, apt.cache.FetchFailedException), e:
  253.             logging.warning('Package fetching failed: %s', str(e))
  254.             raise SystemError, str(e)
  255.         return True
  256.  
  257.     def remove_package(self, package, progress_cb):
  258.         '''Uninstall the given package.
  259.  
  260.         As this is called in the backend, this must happen noninteractively.
  261.         For progress reporting, progress_cb(current, total) is called
  262.         regularly. Passes '-1' for current and/or total if time cannot be
  263.         determined.
  264.  
  265.         If this succeeds, subsequent package_installed(package) calls must
  266.         return False.
  267.  
  268.         Any removal failure should be raised as a SystemError.
  269.         '''
  270.         os.environ['DEBIAN_FRONTEND'] = 'noninteractive'
  271.         os.environ['PATH'] = '/sbin:/usr/sbin:/bin:/usr/bin'
  272.         
  273.         class MyInstallProgress(_CapturedInstallProgress):
  274.             def __init__(self, callback):
  275.                 _CapturedInstallProgress.__init__(self)
  276.                 self.callback = callback
  277.  
  278.             def statusChange(self, pkg, percent, status):
  279.                 logging.debug('remove progress statusChange %s %f' % (pkg, percent))
  280.                 self.callback(percent, 100.0)
  281.  
  282.         logging.debug('Removing package: %s', package)
  283.  
  284.         c = apt.Cache()
  285.         try:
  286.             try:
  287.                 c[package].markDelete()
  288.             except KeyError:
  289.                 logging.debug('Package %s does not exist, aborting', package)
  290.                 return False
  291.             inst_p = progress_cb and MyInstallProgress(progress_cb) or None
  292.             c.commit(None, inst_p)
  293.             if inst_p:
  294.                 inst_p.stdout.seek(0)
  295.                 out = inst_p.stdout.read()
  296.                 inst_p.stdout.close()
  297.                 inst_p.stderr.seek(0)
  298.                 err = inst_p.stderr.read()
  299.                 inst_p.stderr.close()
  300.  
  301.                 if out:
  302.                     logging.debug(out)
  303.                 if err:
  304.                     logging.error(err)
  305.         except apt.cache.LockFailedException, e:
  306.             logging.debug('could not lock apt cache, aborting: %s', str(e))
  307.             raise SystemError, str(e)
  308.  
  309.         return True
  310.  
  311.     def packaging_system(self):
  312.         '''Return packaging system.
  313.  
  314.         Currently defined values: apt, yum
  315.         '''
  316.         if os.path.exists('/etc/apt/sources.list') or os.path.exists(
  317.             '/etc/apt/sources.list.d'):
  318.             return 'apt'
  319.         elif os.path.exists('/etc/yum.conf'):
  320.             return 'yum'
  321.  
  322.         raise NotImplementedError, 'local packaging system is unknown'
  323.  
  324.     # 
  325.     # The following functions MUST be implemented by distributors
  326.     # Note that apt and yum PackageKit backends currently do not implement
  327.     # RepoSetData(), so those need to remain package system specific
  328.     #
  329.  
  330.     def add_repository(self, repository):
  331.         '''Add a repository.
  332.  
  333.         The format for repository is distribution specific. This function
  334.         should also download/update the package index for this repository.
  335.  
  336.         This should throw a ValueError if the repository is invalid or
  337.         inaccessible.
  338.         '''
  339.         if self.repository_enabled(repository):
  340.             logging.debug('add_repository(%s): already active', repository)
  341.             return
  342.  
  343.         if os.path.exists(self.apt_jockey_source):
  344.             backup = self.apt_jockey_source + '.bak'
  345.             os.rename(self.apt_jockey_source, backup)
  346.         else:
  347.             backup = None
  348.         f = open(self.apt_jockey_source, 'w')
  349.         print >> f, repository.strip()
  350.         f.close()
  351.  
  352.         try:
  353.             # TODO: progress feedback
  354.             c = apt.Cache()
  355.             c.update()
  356.         except SystemError, e:
  357.             logging.error('add_repository(%s): Invalid repository', repository)
  358.             if backup:
  359.                 os.rename(backup, self.apt_jockey_source)
  360.             else:
  361.                 os.unlink(self.apt_jockey_source)
  362.             raise ValueError(e.message)
  363.         except apt.cache.FetchCancelledException, e:
  364.             return False
  365.         except (apt.cache.LockFailedException, apt.cache.FetchFailedException), e:
  366.             logging.warning('Package fetching failed: %s', str(e))
  367.             raise SystemError, str(e)
  368.  
  369.     def remove_repository(self, repository):
  370.         '''Remove a repository.
  371.  
  372.         The format for repository is distribution specific.
  373.         '''
  374.         if not os.path.exists(self.apt_jockey_source):
  375.             return
  376.         result = []
  377.         for line in open(self.apt_jockey_source):
  378.             if line.strip() != repository:
  379.                 result.append(line)
  380.         if result:
  381.             f = open(self.apt_jockey_source, 'w')
  382.             f.write('\n'.join(result))
  383.             f.close()
  384.         else:
  385.             os.unlink(self.apt_jockey_source)
  386.  
  387.     def repository_enabled(self, repository):
  388.         '''Check if given repository is enabled.'''
  389.  
  390.         for f in [self.apt_sources] + glob(self.apt_sources + '.d/*.list'):
  391.             try:
  392.                 logging.debug('repository_enabled(%s): checking %s', repository, f)
  393.                 for line in open(f):
  394.                     if line.strip() == repository:
  395.                         logging.debug('repository_enabled(%s): match', repository)
  396.                         return True
  397.             except IOError:
  398.                 pass
  399.         logging.debug('repository_enabled(%s): no match', repository)
  400.         return False
  401.  
  402.     def ui_help_available(self, ui):
  403.         '''Return if help is available.
  404.  
  405.         This gets the current UI object passed, which can be used to determine
  406.         whether GTK/KDE is used, etc.
  407.         '''
  408.         return os.access('/usr/bin/yelp', os.X_OK)
  409.  
  410.     def ui_help(self, ui):
  411.         '''The UI's help button was clicked.
  412.  
  413.         This should open a help HTML page or website, call yelp with an
  414.         appropriate topic, etc. This gets the current UI object passed, which
  415.         can be used to determine whether GTK/KDE is used, etc.
  416.         '''
  417.         if 'gtk' in str(ui.__class__).lower():
  418.             import gobject
  419.             gobject.spawn_async(['yelp', 'ghelp:hardware#jockey'],
  420.                 flags=gobject.SPAWN_SEARCH_PATH)
  421.  
  422.     # 
  423.     # The following functions have a reasonable default implementation for
  424.     # Linux, but can be tweaked by distributors
  425.     #
  426.  
  427.     def set_backup_dir(self):
  428.         '''Setup self.backup_dir, directory where backup files are stored.
  429.         
  430.         This is used for old xorg.conf, DriverDB caches, etc.
  431.         '''
  432.         self.backup_dir = '/var/cache/jockey'
  433.         if not os.path.isdir(self.backup_dir):
  434.             try:
  435.                 os.makedirs(self.backup_dir)
  436.             except OSError, e:
  437.                 logging.error('Could not create %s: %s, using temporary '
  438.                     'directory; all your caches will be lost!',
  439.                     self.backup_dir, str(e))
  440.                 self.backup_dir = tempfile.mkdtemp(prefix='jockey_cache')
  441.  
  442.     def ignored_modules(self):
  443.         '''Return a set of kernel modules which should be ignored.
  444.  
  445.         This particularly effects free kernel modules which are shipped by the
  446.         OS vendor by default, and thus should not be controlled with this
  447.         program.  Since this will include the large majority of existing kernel
  448.         modules, implementing this is also important for speed reasons; without
  449.         it, detecting existing modules will take quite long.
  450.         
  451.         Note that modules which are ignored here, but covered by a custom
  452.         handler will still be considered.
  453.         '''
  454.         # try to get a *.ko file list from the main kernel package to avoid testing
  455.         # known-free drivers
  456.         dpkg = subprocess.Popen(['dpkg', '-L', 'linux-image-' + os.uname()[2]],
  457.             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  458.         out = dpkg.communicate()[0]
  459.         result = set()
  460.         if dpkg.returncode == 0:
  461.             for l in out.splitlines():
  462.                 if l.endswith('.ko'):
  463.                     result.add(os.path.splitext(os.path.basename(l))[0].replace('-', '_'))
  464.  
  465.         dpkg = subprocess.Popen(['dpkg', '-L', 'linux-ubuntu-modules-' + os.uname()[2]],
  466.             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  467.         out = dpkg.communicate()[0]
  468.         if dpkg.returncode == 0:
  469.             for l in out.splitlines():
  470.                 if l.endswith('.ko'):
  471.                     result.add(os.path.splitext(os.path.basename(l))[0].replace('-', '_'))
  472.  
  473.         return result
  474.  
  475.     def module_blacklisted(self, module):
  476.         '''Check if a module is on the modprobe blacklist.'''
  477.  
  478.         return module in self._module_blacklist or \
  479.             module in self._module_blacklist_system
  480.  
  481.     def blacklist_module(self, module, blacklist):
  482.         '''Add or remove a kernel module from the modprobe blacklist.
  483.         
  484.         If blacklist is True, the module is blacklisted, otherwise it is
  485.         removed from the blacklist.
  486.         '''
  487.         if blacklist:
  488.             self._module_blacklist.add(module)
  489.         else:
  490.             try:
  491.                 self._module_blacklist.remove(module)
  492.             except KeyError:
  493.                 return # no need to save the blacklist
  494.  
  495.         self._save_module_blacklist()
  496.  
  497.     def _load_module_blacklist(self):
  498.         '''Initialize self._module_blacklist{,_system}.'''
  499.  
  500.         self._module_blacklist = set()
  501.         self._module_blacklist_system = set()
  502.  
  503.         self._read_blacklist_file(self.module_blacklist_file, self._module_blacklist)
  504.  
  505.         # read other blacklist files (which we will not touch, but evaluate)
  506.         for f in glob('%s/blacklist*' % os.path.dirname(self.module_blacklist_file)):
  507.             if f != self.module_blacklist_file:
  508.                 self._read_blacklist_file(f, self._module_blacklist_system)
  509.  
  510.     @classmethod
  511.     def _read_blacklist_file(klass, path, blacklist_set):
  512.         '''Read a blacklist file and add modules to blacklist_set.'''
  513.  
  514.         try:
  515.             f = open(path)
  516.         except IOError:
  517.             return
  518.  
  519.         try:
  520.             fcntl.flock(f.fileno(), fcntl.LOCK_SH)
  521.             for line in f:
  522.                 # strip off comments
  523.                 line = line[:line.find('#')].strip()
  524.  
  525.                 if not line.startswith('blacklist'):
  526.                     continue
  527.  
  528.                 module = line[len('blacklist'):].strip()
  529.                 if module:
  530.                     blacklist_set.add(module)
  531.         finally:
  532.             f.close()
  533.  
  534.     def _save_module_blacklist(self):
  535.         '''Save module blacklist.'''
  536.  
  537.         if len(self._module_blacklist) == 0 and \
  538.             os.path.exists(self.module_blacklist_file):
  539.                 os.unlink(self.module_blacklist_file)
  540.                 return
  541.  
  542.         os.umask(022)
  543.         # create directory if it does not exist
  544.         d = os.path.dirname(self.module_blacklist_file)
  545.         if not os.path.exists(d):
  546.             os.makedirs(d)
  547.  
  548.         f = open(self.module_blacklist_file, 'w')
  549.         try:
  550.             fcntl.flock(f.fileno(), fcntl.LOCK_EX)
  551.             for module in sorted(self._module_blacklist):
  552.                 print >> f, 'blacklist', module
  553.         finally:
  554.             f.close()
  555.  
  556.     def _get_os_version(self):
  557.         '''Initialize self.os_vendor and self.os_version.
  558.  
  559.         This defaults to reading the values from lsb_release.
  560.         '''
  561.         p = subprocess.Popen(['lsb_release', '-si'], stdout=subprocess.PIPE,
  562.             stderr=subprocess.PIPE, close_fds=True)
  563.         self.os_vendor = p.communicate()[0].strip()
  564.         p = subprocess.Popen(['lsb_release', '-sr'], stdout=subprocess.PIPE,
  565.             stderr=subprocess.PIPE, close_fds=True)
  566.         self.os_version = p.communicate()[0].strip()
  567.         assert p.returncode == 0
  568.  
  569.     def get_system_vendor_product(self):
  570.         '''Return (vendor, product) of the system hardware.
  571.  
  572.         Either or both can be '' if they cannot be determined.
  573.  
  574.         The default implementation queries hal.
  575.         '''
  576.  
  577.         try:
  578.             hal = subprocess.Popen([self.hal_get_property_path, '--udi',
  579.                 '/org/freedesktop/Hal/devices/computer', '--key',
  580.                 'system.hardware.vendor'], stdout=subprocess.PIPE,
  581.                 close_fds=True)
  582.             vendor = hal.communicate()[0].strip()
  583.             assert hal.returncode == 0
  584.         except (OSError, AssertionError):
  585.             vendor = ''
  586.  
  587.         try:
  588.             hal = subprocess.Popen([self.hal_get_property_path, '--udi',
  589.                 '/org/freedesktop/Hal/devices/computer', '--key',
  590.                 'system.hardware.product'], stdout=subprocess.PIPE,
  591.                 close_fds=True)
  592.             product = hal.communicate()[0].strip()
  593.             assert hal.returncode == 0
  594.         except (OSError, AssertionError):
  595.             product = ''
  596.  
  597.         return (vendor, product)
  598.